package eu.webhib.extgui.controller.tree;

import eu.webhib.extgui.controller.intf.TreeDataProvider;
import eu.webhib.extgui.data.intf.GuiBaseEntity;
import eu.webhib.extgui.data.intf.DataItem;
import eu.webhib.extgui.update.UpdateMngr;
import eu.webhib.extgui.utils.GuiRequestClosure;
import org.apache.log4j.Logger;
import org.primefaces.event.*;
import org.primefaces.model.*;

/**
 * View controller managing a tree view.
 * Supports an selected tree node and update of the tree view if required.
 * An selected tree node cannot be deselected by the user.
 */
public class TreeController {

   private static final Logger log = Logger.getLogger(TreeController.class);
   /** providing all items of the tree */
   protected TreeDataProvider treeDataProvider;
   /** controls the partial update of view components */
   protected UpdateMngr updateMngr;
   /** dummy master tree root node for view */
   private TreeNode masterTreeRoot = null;
   /** data item root */
   protected DataItem dataItemTreeRoot;
   /** tree root node under master root node */
   protected TreeNode treeRoot;
   /** currently selected tree node if any */
   protected TreeNode selectedTreeNode = null;

   /**
    * @param treeDataProvider the data provider
    * @param updateMngr controls update of view components
    */
   public TreeController(TreeDataProvider treeDataProvider, UpdateMngr updateMngr) {
      this.treeDataProvider = treeDataProvider;
      this.updateMngr = updateMngr;
   }

   /**
    * Build up or rebuild the whole tree
    */
   public void buildData() {
      log.debug("buildData");

      /** dummy master root of overall tree, not visible in view but needed */
      masterTreeRoot = new DefaultTreeNode("TreeRootDummy", null);
      masterTreeRoot.setExpanded(true);

      /** data item root */
      dataItemTreeRoot = treeDataProvider.getRoot();

      /** create tree node under master root node and insert data item root node */
      treeRoot = new DefaultTreeNode(dataItemTreeRoot, masterTreeRoot);
      /** create tree node structure based on data item children relationship */
      buildTree(dataItemTreeRoot, treeRoot);
   }

   /**
    * Helper method to construct recursively the tree nodes based on and associated with the provided data items
    * @param dataItem holding the data item together with its children
    * @param treeNode the tree node associated with the dataItem parameter
    */
   protected void buildTree(DataItem dataItem, TreeNode treeNode) {
      log.trace("buildTree: " + dataItem);

      for (DataItem childItem : treeDataProvider.getViewChildren(dataItem)) {
         /** construct new node as children of treeNode */
         TreeNode childNode = new DefaultTreeNode(childItem, treeNode);
         /** call recursively */
         buildTree(childItem, childNode);
      }
   }

   /**
    * Called by outside to rebuild all internal data structures to update the whole
    * tree view. The currently selected node will hold its selection.
    */
   public void refreshData() {
      log.info("refreshData");

      buildData();
      /** keep previous selection */
      if (selectedTreeNode == null) {
         setRootSelected();
      } else {
         /** since the tree nodes are rebuild (new object) need to
          *  lookup the data item to get the tree node to select */
         setSelectedTreeNodeByData((DataItem) selectedTreeNode.getData());
      }
   }

   /**
    * Called by outside to set the root node as the selected node
    */
   public void setRootSelected() {
      treeRoot.setSelected(true);
      treeRoot.setExpanded(true);
      setSelectedTreeNodeInternal(treeRoot);
   }

   /**
    * Called by view to get the tree root node
    * @return the root node of the tree
    */
   public TreeNode getRootNode() {
      log.info("getRootNode: " + (masterTreeRoot == null ? "null" : masterTreeRoot.getData().toString()));

      /** returns master tree root node */
      return masterTreeRoot;
   }

   /**
    * Called by view respectability tree framework
    * @return the currently selected tree node
    */
   public TreeNode getSelectedTreeNode() {
      log.info("getSelectedTreeNode: " + selectedTreeNode.getData().toString());

      return selectedTreeNode;
   }

   /**
    * Called by view respectability tree framework
    * @param selectedTreeNode
    */
   public void setSelectedTreeNode(TreeNode selectedTreeNode) {
      /** only provided to fulfill contract with view, but should not do anything,
       * as setSelectedTreeNodeInternal is called by onNodeSelect/ Deselect listeners */
   }

   /**
    * Tree listener called by view on node selection
    * @param event
    */
   public void onNodeSelect(NodeSelectEvent event) {
      log.info("onNodeSelect: " + event.getTreeNode().getData().toString());

      /** tree node clicked by user */
      TreeNode clickedNode = event.getTreeNode();
      /** set effectively selected node */
      setSelectedTreeNodeInternal(clickedNode);
   }

   /**
    * helper method: set the effectively selected node, is overwritten by derived class
    * @param selectedTreeNode the node that is seen as be selected
    */
   protected void setSelectedTreeNodeInternal(TreeNode selectedTreeNode) {
      log.info("setSelectedTreeNodeInternal: " + (selectedTreeNode == null ? "" : selectedTreeNode.getData().toString()));

      /** remember the currently selected tree node */
      this.selectedTreeNode = selectedTreeNode;
   }

   /**
    * Tree listener called by view on node deselection.
    * Avoids deselection of already selected node
    * @param event
    */
   public void onNodeUnselect(NodeUnselectEvent event) {
      log.info("onNodeUnselect: " + event.getTreeNode().getData().toString());

      /** tree node clicked by user that is already selected */
      TreeNode clickedNode = event.getTreeNode();
      /** undo deselection */
      clickedNode.setSelected(true);
      /** reread data */
      refreshData();

      /** update all view elements defined by updateMngr */
      updateMngr.updateAll();
   }

   /** TBC */
   public void onDragDrop(final DragDropEvent event) {

      new GuiRequestClosure() {

         @Override
         protected void run() {
            doDragDrop(event);
         }
      }.start();
   }

   private void doDragDrop(DragDropEvent event) {
      TreeNode node = (TreeNode) event.getData();
      log.info("onDragDrop " + event.getDragId() + " " + event.getDropId());
      log.debug("node: " + node + " node.getParent: " + node.getParent());
      log.debug("data " + node.getData() + " viewparent.data " + treeDataProvider.getViewParent((DataItem) node.getData()));

      if (treeDataProvider.moveTo((DataItem) node.getData(), (DataItem) node.getParent().getData())) {
         /** reread data */
         refreshData();
         /** was an allowed operation, so reselect selection (needed for connected list) */
      } else {
         /** for the moment in this situation the action listener is not invoked by primefaces ***/
         DataItem oldParent = treeDataProvider.getViewParent((DataItem) node.getData());
         log.debug("onDragDrop: " + oldParent);
         /** manually correct the already performed relationship change */
         node.setParent(findTreeNodeByData(oldParent));
         /** update all view elements to redisplay corrected situation */
      }
      updateMngr.updateAll();
   }

   /**
    * Expand all tree nodes starting with the given node up to the root
    * @param node
    */
   public void expandedUpToRoot(TreeNode node) {
      while (node != null) {
         node.setExpanded(true);
         node = node.getParent();
      }
   }

   /**
    * Retrieve data item represented by the currently selected tree node
    * @return Data item of the currently selected tree node
    */
   public DataItem getSelectedDataItem() {
      return (DataItem) selectedTreeNode.getData();
   }

   /**
    * Retrieve the data item of the currently selected tree node or null if it is not an entity
    * @return Entity of the currently selected tree node
    */
   public GuiBaseEntity getSelectedEntity() {
      DataItem selectedEntity = this.getSelectedDataItem();
      log.info("getSelectedEntity - selectedEntity: " + selectedEntity);
      if (selectedEntity == dataItemTreeRoot) {
         return null;
      } else {
         return (GuiBaseEntity) selectedEntity;
      }
   }

   /**
    * Set the tree node as selected holding the given data item if found
    * @param dataItem whose tree node should be selected
    * @return if selection was successful
    */
   public boolean setSelectedTreeNodeByData(DataItem dataItem) {
      log.info("setSelectedTreeNodeByData: " + dataItem);

      /** search for node */
      TreeNode foundNode = findTreeNodeByData(dataItem);
      if (foundNode == null) {
         log.info("setSelectedTreeNodeByDataItem - not found");
         return false;
      }
      /** deselect previous selected if any */
      if (this.selectedTreeNode != null) {
         this.selectedTreeNode.setSelected(false);
      }
      setSelectedTreeNodeInternal(foundNode);
      foundNode.setSelected(true);
      /** expand to show the selected node */
      expandedUpToRoot(foundNode);
      /** selection was successful */
      return true;
   }

   /**
    * Search for the tree node holding the given data item
    * @param dataItem item to search for
    * @return the tree node found, null if not found
    */
   public TreeNode findTreeNodeByData(DataItem dataItem) {
      return findTreeNodeByData(treeRoot, dataItem);
   }

   /**
    * Internal helper: Search for the tree node holding the given data item starting
    * at the given tree node
    * @param searchNode tree node to start to search
    * @param dataItem item to search for
    * @return the tree node found, null if not found
    */
   protected TreeNode findTreeNodeByData(TreeNode searchNode, DataItem dataItem) {
      /** check if both represent the same data item */
      if (((DataItem) searchNode.getData()).identical(dataItem)) {
         return searchNode;
      }
      for (TreeNode childNode : searchNode.getChildren()) {
         /** call recursively */
         TreeNode foundNode = findTreeNodeByData(childNode, dataItem);
         if (foundNode != null) {
            return foundNode;
         }
      }
      return null;
   }
}
